import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.FileOutputStream;

public class BufferPool {

	private int block_size;
	private BufferNode head, tail;
	private int hits, misses, reads, writes;
	private int max_count, cur_count;
	private String file_name;
	private RandomAccessFile file;

	public BufferPool(int num_blocks, int block_size, String file_name)
			throws FileNotFoundException {
		max_count = num_blocks;
		this.block_size = block_size;
		File f = new File(file_name);
		if(f.exists() && f.canWrite() && !f.isDirectory())
			f.delete();
		this.file_name = file_name;
		file = new RandomAccessFile(file_name, "rws");
		initCacheCount();
		cur_count = 0;
		head = tail = null;
	}

	public int read(Integer addr, byte[] buff) throws IOException {
		BufferNode temp = checkHit(addr);
		if (temp == null) {
			if (cur_count == max_count)
				removeLRU();
			temp = makeNew(addr);
		}
		if (cur_count > 1)
			movetoTop(temp);
		int length = temp.read(addr, buff);
		int remaining = (addr % block_size + length) - (block_size - 1);
		int i = 1;
		while (remaining > 0) {
			temp = null;
			temp = checkHit(addr + block_size * i);
			if (temp == null) {
				if (cur_count == max_count)
					removeLRU();
				temp = makeNew(addr + block_size * i);
			}
			if (cur_count > 1)
				movetoTop(temp);
			temp.read(buff, length - remaining, remaining);
			i++;
			remaining -= block_size;
		}
		return length;
	}

	public void write(Integer addr, byte[] buff, byte length) throws IOException {
		BufferNode temp = checkHit(addr);
		if (temp == null) {
			if (cur_count == max_count)
				removeLRU();
			temp = makeNew(addr);
		}
		if (cur_count > 1)
			movetoTop(temp);
		temp.setDirty();
		int written = temp.write(addr, buff, length);
		int i = 1;
		while(written < length)
		{
			temp = null;
			temp = checkHit(addr + block_size * i);
			if (temp == null) {
				if (cur_count == max_count)
					removeLRU();
				temp = makeNew(addr + block_size * i);
			}
			if (cur_count > 1)
				movetoTop(temp);
			temp.setDirty();
			written = temp.write(buff, written, length);
		}
		//System.out.println("write " + addr + " " + length);
	}

	public void flush() {
		for (BufferNode cur = head; cur != null; cur = cur.getNext())
			if (cur.isDirty())
				cur.flush();
	}

	public void close() {
		try {
			file.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private BufferNode makeNew(int addr) throws IOException {
		BufferNode temp = new BufferNode(addr, null, head);
		if (head != null)
			head.setPrev(temp);
		head = temp;
		if (cur_count == 0) {
			tail = temp;
		}
		cur_count++;
		return temp;
	}

	private void movetoTop(BufferNode it) {
		if (it.equals(head)) // if it is already at top, do nothing.
		{
			return;
		}
		if (it.equals(tail)) {
			// in this case, it will be the tail but the length of
			// the array will be > 1. Set prev element as the new
			// tail.
			it.getPrev().setNext(null);
			tail = it.getPrev();
		} else {
			// at this point, it cannot be the head or the tail, and therefore
			// must have valid a prev next nodes. This part essentially
			// "removes" it from the position in the array
			it.getPrev().setNext(it.getNext());
			it.getNext().setPrev(it.getPrev());
		}
		// at this point it needs to be set as the new head of the array, since
		// it was the most recently accessed node.
		it.setPrev(null);
		it.setNext(head);
		head.setPrev(it);
		head = it;
		return;
	}

	private void removeLRU() {
		if (tail.isDirty())
			tail.flush();
		if (cur_count == 1)
			head = tail = null;
		else {
			tail = tail.getPrev();
			tail.setNext(null);
		}
		cur_count--;
	}

	private BufferNode checkHit(int addr) {
		for (BufferNode cur = head; cur != null; cur = cur.getNext())
			if (cur.checkAddr(addr)) {
				hits++;
				return cur;
			}
		misses++;
		return null;
	}

	public void initCacheCount() {
		hits = misses = reads = writes = 0;
	}

	public int getHits() {
		return hits;
	}

	public int getMisses() {
		return misses;
	}

	public int getReads() {
		return reads;
	}

	public int getWrites() {
		return writes;
	}
	
	public void printBlockIDs()
	{
		System.out.println("Block ID's currently contained in the buffer pool");
		System.out.println("(from most recently used to least recently used)");
		for(BufferNode cur = head; cur != null; cur = cur.getNext())
			System.out.print(cur.getBlockID() + " ");
		System.out.println();
	}

	public void clear() throws IOException {
		initCacheCount();
		cur_count = 0;
		head = tail = null;
	}

	private class BufferNode {
		private byte[] block;
		private int addresshead;
		private boolean dirty;
		private BufferNode prev, next;

		public BufferNode(int addr, BufferNode pr, BufferNode nx)
				throws IOException {
			block = new byte[block_size];
			addresshead = addr / block_size;
			file.seek(addresshead * block_size);
			file.read(block);
			reads++;
			prev = pr;
			next = nx;
		}

		public void flush() {
			try {
				file.seek(addresshead * block_size);
				file.write(block, 0, block_size);
				writes++;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		public int read(int addr, byte[] buff) {
			int length = block[addr % block_size];
			for (int i = 1; (i <= length)
					&& (addr % block_size + i) < block_size; i++)
				buff[i - 1] = block[addr % block_size + i];
			return length;
		}

		public void read(byte[] buff, int offset, int length) {
			for (int i = 0; i < length && i < block_size; i++)
				buff[i + offset] = block[i];
		}

		public int write(int addr, byte[] buff, byte length) {
			block[addr % block_size] = length;
			int i;
			for(i = 0; i < length && (addr % block_size + i + 1) < block_size; i++)
			{
				block[addr % block_size + i + 1] = buff[i];
			}
			return i;
		}
		
		public int write(byte[] buff, int offset, byte length)
		{
			int i;
			for(i = 0; offset + i < length && i < block_size; i++)
			{
				block[i] = buff[offset + i];
			}
			return offset + i;
		}

		public boolean checkAddr(int addr) {
			return addresshead == addr / block_size;
		}

		public boolean isDirty() {
			return dirty;
		}

		public void setDirty() {
			dirty = true;
		}

		public BufferNode getPrev() {
			return prev;
		}

		public void setPrev(BufferNode prev) {
			this.prev = prev;
		}

		public BufferNode getNext() {
			return next;
		}

		public void setNext(BufferNode next) {
			this.next = next;
		}

		public boolean equals(BufferNode it) {
			if (it == null)
				return false;
			return this.addresshead == it.addresshead;
		}
		
		public int getBlockID()
		{
			return addresshead;
		}
	}

	public void clearFile() throws IOException {
		file.setLength(0);
	}
}
